<?php
/**
 * Copyright (c) 2020
 * 摘    要：
 * 作    者：san
 * 修改日期：2020.04.14
 */

namespace App\Service;

use App\Library\Clog\Log;
use App\Library\AutoDs\Folder;
use App\Library\AutoDs\Command;
use App\Library\AutoDs\Repo;
use App\Library\AutoDs\Task as AutodsTask;
use App\Library\Email\CEmail;
use App\Model\Environment;
use App\Model\Project;
use App\Model\Record;
use App\Model\Task;
use App\Model\User;
use ErrorException;
use Exception;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\Model;
use phpDocumentor\Reflection\Types\Context;

class DeployService extends BaseService
{
    /**
     * @var Task $task
     */
    protected $task;
    /**
     * @var Project $conf
     */
    protected $conf;

    /**
     * @var Folder $autodsFolder
     */
    protected $autodsFolder;

    /**
     * @var AutodsTask $autodsTask
     */
    protected $autodsTask;

    /**
     * DeployService constructor.
     */
    public function __construct()
    {
        parent::__construct();

        $this->redis = redis();
    }

    /**
     * 获取任务单部署执行记录
     *
     * @param $taskId
     * @param $uid
     * @throws ErrorException
     * @return array|bool
     */
    public function getTaskDeployRecord($taskId, $uid)
    {
        $task = $this->_checkTaskExits($taskId);

        if ($task->user_id != $uid) {
            throw new ErrorException(t('message.12059'));
        }

        $record = Record::query(true)
            ->where('task_id', $taskId)
            ->orderBy('action', 'asc')
            ->get();

        if ($record) {
            return $record->toArray();
        }

        return [];
    }

    /**
     * @param $taskId
     * @throws ErrorException
     * @return Builder|Builder[]|Collection|Model|null
     */
    private function _checkTaskExits($taskId)
    {
        $task = Task::query(true)->find($taskId);

        if (!$task) {
            throw new ErrorException(t('message.12031'));
        }

        return $task;
    }

    /**
     * 获取task 发布进度
     *
     * @param $taskId
     * @param $lastId
     * @return array
     */
    public function getProcess($taskId, $lastId = 0)
    {
        try {
            $query = Record::query()
                ->select(['id', 'action', 'status', 'memo', 'command'])
                ->where(['task_id' => $taskId]);

            if ($lastId) {
                $query = $query->where('id', '>', $lastId);
            }

            $record = $query->orderBy('id', 'asc')->firstOrFail();

            $record            = $record->toArray();
            $record['memo']    = stripslashes($record['memo'] ?? '');
            $record['command'] = stripslashes($record['command'] ?? '');

            return $record;
        } catch (\Exception $exception) {
            return [];
        }
    }


    /**
     * 执行部署
     *
     * @param $taskId
     * @param $uuid
     * @throws ErrorException
     * @throws Exception
     * @return bool
     */
    public function deploy($taskId, $uuid)
    {
        $this->task = TaskService::_checkExits($taskId);
        $this->conf = Project::getInstance($this->task->project_id);

        $this->autodsTask   = new AutodsTask($this->conf);
        $this->autodsFolder = new Folder($this->conf);

        if (!$this->conf) {
            throw new ErrorException(t('message.12030'));
        }

        if ($this->task->user_id != $uuid) {
            throw new ErrorException(t('message.12028'));
        }

        //任务失败或者审核通过时可发起上线
        if (!in_array($this->task->status, [Task::STATUS_PASS, Task::STATUS_FAILED])) {
            throw new Exception(t('message.12029'));
        }
        //部署前再次刷新host
        ProjectService::_saveAnsibleHosts($this->conf);
        // 清除历史记录
        Record::query()->where(['task_id' => $this->task->id])->delete();

        $user = User::query()->find($uuid);
        $to   = $user->email;
        //正式环境发布需要抄送全体技术人员
        $cc      = $this->conf->level == Environment::PRO_ENV ? 'test@qq.com' : '';
        $message = [
            'level'        => Environment::getNamesMap($this->conf->level),
            'project_name' => $this->conf->name,
            'title'        => $this->task->title,
            'commit_id'    => $this->task->commit_id,
            'user'         => $user->nick_name,
        ];
        try {
            //上线操作
            if ($this->task->action == Task::ACTION_ONLINE) {
                $this->_makeVersion();
                $this->_initWorkspace();
                $this->_preDeploy();
                $this->_revisionUpdate();
                $this->_postDeploy();
                $this->_transmission();
                $this->_updateRemoteServers($this->task->link_id, $this->conf->post_release_delay, $this->task->gray_push);
                $this->_cleanRemoteReleaseVersion($this->task->gray_push);
                $this->_cleanUpLocal($this->task->link_id);
            } else {
                $this->_rollback($this->task->ex_link_id, $this->task->gray_push);
            }
            //收尾工作
            $this->_endingWork();
            // 可回滚的版本设置
            $this->_enableRollBack();
            //成功邮件
            $this->_sendEmail('deploy_success', $message, $to, $cc);
        } catch (Exception $exception) {
            Log::exception($exception);
            $this->task->status = Task::STATUS_FAILED;
            $this->task->save();
            // 清理本地
            $this->_cleanUpLocal($this->task->link_id);
            //发送失败邮件
            $this->_sendEmail('deploy_error', $message, $to, $cc);
            throw $exception;
        }
        //发送成功邮件
        return true;
    }

    /**
     * 产生一个上线版本
     */
    private function _makeVersion()
    {
        $version             = date("Ymd-His", time());
        $this->task->link_id = $version;

        return $this->task->save();
    }

    /**
     * 检查目录和权限
     *
     * @throws Exception
     * @return bool
     */
    private function _initWorkspace()
    {
        // 本地宿主机工作区初始化
        $this->autodsFolder->initLocalWorkspace($this->task);

        // 远程目标目录检查，并且生成版本目录
        $sTime    = Command::getMs();
        $ret      = $this->autodsFolder->initRemoteVersion($this->task->link_id, $this->task->gray_push);
        $duration = Command::getMs() - $sTime;
        Record::saveRecord($this->task->user_id, $this->autodsFolder, $this->task->id, Record::ACTION_PERMSSION, $duration);

        if (!$ret) {
            throw new Exception(t('message.12032'));
        }

        return true;
    }

    /**
     * 部署前置触发任务
     * 在部署代码之前的准备工作，如git的一些前置检查、vendor的安装（更新）
     *
     * @throws Exception
     * @return bool
     */
    private function _preDeploy()
    {
        $sTime = Command::getMs();
        $ret   = $this->autodsTask->preDeploy($this->task->link_id);

        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        Record::saveRecord($this->task->user_id, $this->autodsTask, $this->task->id, Record::ACTION_PRE_DEPLOY, $duration);

        if (!$ret) {
            throw new Exception(t('message.12033'));
        }

        return true;
    }

    /**
     * 更新代码文件
     *
     * @throws Exception
     * @return bool
     */
    private function _revisionUpdate()
    {
        // 更新代码文件
        $revision = Repo::getInstance($this->conf);
        $sTime    = Command::getMs();
        $ret      = $revision->updateToVersion($this->task); // 更新到指定版本
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        Record::saveRecord($this->task->user_id, $revision, $this->task->id, Record::ACTION_CLONE, $duration);

        if (!$ret) {
            throw new Exception(t('message.12034'));
        }

        return true;
    }

    /**
     * 部署后置触发任务
     * git代码检出之后，可能做一些调整处理，如vendor拷贝，配置环境适配（mv config-test.php config.php）
     *
     * @throws Exception
     * @return bool
     */
    private function _postDeploy()
    {
        $sTime = Command::getMs();
        $ret   = $this->autodsTask->postDeploy($this->task->link_id);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        Record::saveRecord($this->task->user_id, $this->autodsTask, $this->task->id, Record::ACTION_POST_DEPLOY, $duration);

        if (!$ret) {
            throw new Exception(t('message.12035'));
        }

        return true;
    }

    /**
     * 传输文件/目录到指定目标机器
     *
     * @throws Exception
     * @return bool
     */
    private
    function _transmission()
    {
        $sTime = Command::getMs();

        if (Project::getAnsibleStatus()) {
            // ansible copy
            $this->autodsFolder->ansibleCopyFiles($this->conf, $this->task);
        } else {
            // 循环 scp
            $this->autodsFolder->scpCopyFiles($this->conf, $this->task);
        }

        // 记录执行时间
        $duration = Command::getMs() - $sTime;

        Record::saveRecord($this->task->user_id, $this->autodsFolder, $this->task->id, Record::ACTION_SYNC, $duration);

        return true;
    }

    /**
     * 执行远程服务器任务集合
     * 对于目标机器更多的时候是一台机器完成一组命令，而不是每条命令逐台机器执行
     *
     * @param string $version
     * @param integer $delay 每台机器延迟执行post_release任务间隔, 不推荐使用, 仅当业务无法平滑重启时使用
     * @param bool $grayPush 是否灰度发布
     * @throws Exception
     * @return bool
     */
    private function _updateRemoteServers($version, $delay = 0, $grayPush = false)
    {
        $cmd = [];
        // pre-release task
        if (($preRelease = autodsTask::getRemoteTaskCommand($this->conf->pre_release, $version))) {
            $cmd[] = $preRelease;
        }
        // link
        if (($linkCmd = $this->autodsFolder->getLinkCommand($version))) {
            $cmd[] = $linkCmd;
        }
        // post-release task
        if (($postRelease = autodsTask::getRemoteTaskCommand($this->conf->post_release, $version))) {
            $cmd[] = $postRelease;
        }

        $sTime = Command::getMs();
        // run the task package
        $ret = $this->autodsTask->runRemoteTaskCommandPackage($cmd, $delay, $grayPush);
        // 记录执行时间
        $duration = Command::getMs() - $sTime;
        Record::saveRecord($this->task->user_id, $this->autodsTask, $this->task->id, Record::ACTION_UPDATE_REMOTE, $duration);

        if (!$ret) {
            throw new Exception(t('message.12036'));
        }

        return true;
    }

    /**
     * 只保留最大版本数，其余删除过老版本
     *
     * @param bool $grayPush 是否灰度发布
     * @throws ErrorException
     * @return bool|int
     */
    private function _cleanRemoteReleaseVersion($grayPush = false)
    {
        return $this->autodsTask->cleanUpReleasesVersion($grayPush);
    }

    /**
     * 收尾工作，清除宿主机的临时部署空间
     *
     * @param null $version
     * @return bool
     */
    private function _cleanUpLocal($version = null)
    {
        // 创建链接指向
        $this->autodsFolder->cleanUpLocal($version);

        return true;
    }

    /**
     * 执行远程服务器任务集合回滚
     *
     * @param $version
     * @param bool $grayPush
     * @throws Exception
     * @return bool
     */
    public function _rollback($version, $grayPush = false)
    {
        return $this->_updateRemoteServers($version, 0, $grayPush);
    }


    /**
     * 可回滚的版本设置
     *
     * @throws ErrorException
     * @return int
     * @return bool
     */
    private function _enableRollBack()
    {
        $condition = [
            'status'     => Task::STATUS_DONE,
            'project_id' => $this->task->project_id,
        ];

        $offset = Task::query(true)
            ->where($condition)
            ->orderBy('id', 'desc')
            ->offset($this->conf->keep_version_num)
            ->limit(1)
            ->value('id');

        if (!$offset) {
            return true;
        }

        $ret = Task::query()
            ->where($condition)
            ->where('id', '<', $offset)
            ->update(['enable_rollback' => Task::ROLLBACK_FALSE]);

        if (!$ret) {
            throw new ErrorException(t('message.12002'));
        }

        return true;
    }

    /**
     * 收尾工作
     */
    private function _endingWork()
    {
        if ($this->task->action == Task::ACTION_ONLINE) {
            $this->task->ex_link_id = $this->task->gray_push ? $this->conf->gray_version : $this->conf->version;
        } elseif ($this->task->action == Task::ACTION_ROLLBACK || $this->task->id == 1) {
            $this->task->enable_rollback = Task::ROLLBACK_FALSE;
        }

        $this->task->status = Task::STATUS_DONE;
        $this->task->save();

        // 记录当前线上版本（软链）回滚则是回滚的版本，上线为新版本
        if ($this->task->gray_push) {
            $this->conf->gray_version = $this->task->link_id;
        } else {
            $this->conf->version = $this->task->link_id;
        }

        $this->conf->save();
    }

    /**
     * 发送邮件
     *
     * @param string $tpl 邮件模板
     * @param array $message 邮件替换内容
     * @param string $to 收件人
     * @param string $cc 抄送人
     */
    private function _sendEmail(string $tpl, array $message, string $to, string $cc = '')
    {
        try {
            $mailer = new CEmail();
            $mailer->compose($tpl, $message)
                ->setFrom()
                ->setTo($to)
                ->setCc($cc)
                ->setSubject('【项目发布结果通知】')
                ->send();
        } catch (\Exception $exception) {
            Log::exception($exception);
        }
    }
}
